/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2020-2020. All rights reserved.
 */

package pl.droidsonroids.gif;

import static pl.droidsonroids.gif.InvalidationHandler.MSG_TYPE_INVALIDATION;

import ohos.aafwk.ability.DataAbilityHelper;
import ohos.aafwk.ability.DataAbilityRemoteException;
import ohos.agp.animation.Animator;
import ohos.agp.components.Component;
import ohos.agp.components.element.Element;
import ohos.agp.components.element.StateElement;
import ohos.agp.render.BlendMode;
import ohos.agp.render.Canvas;
import ohos.agp.render.ColorMatrix;
import ohos.agp.render.Paint;
import ohos.agp.render.PixelMapHolder;
import ohos.agp.utils.Color;
import ohos.agp.utils.Rect;
import ohos.agp.utils.RectFloat;
import ohos.app.Context;
import ohos.global.resource.NotExistException;
import ohos.global.resource.RawFileDescriptor;
import ohos.global.resource.RawFileEntry;
import ohos.global.resource.ResourceManager;
import ohos.global.resource.WrongTypeException;
import ohos.media.image.PixelMap;
import ohos.media.image.common.AlphaType;
import ohos.media.image.common.Size;
import ohos.utils.net.Uri;
import pl.droidsonroids.gif.transforms.CornerRadiusTransform;
import pl.droidsonroids.gif.transforms.Transform;

import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Locale;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class GifDrawable extends StateElement implements MediaPlayerControl {
    /**
     * Transparent
     */
    public static final int TRANSPARENT = -2;
    /**
     * OPAQUE
     */
    public static final int OPAQUE = -1;
    /**
     * default Render Delay
     */
    public static final long RENDER_DELAY = 240L;
    /**
     * default constant
     */
    public static final float DEF_255 = 255;
    /**
     * default value for buf_percent
     */
    public static final int BUFF_PERCENT = 100;

    protected final Paint mPaint = new Paint();

    final ScheduledThreadPoolExecutor mExecutor;

    /**
     * Frame buffer, holds current frame.
     */
    final GifInfoHandle mNativeInfoHandle;
    final ConcurrentLinkedQueue<AnimationListener> mListeners = new ConcurrentLinkedQueue<>();
    final boolean mIsRenderingTriggeredOnDraw;
    final InvalidationHandler mInvalidationHandler;
    volatile boolean mIsRunning = true;
    ScheduledFuture<?> mRenderTaskSchedule;
    PixelMap mPixelMap;
    Component mCallback;
    HashMap<Element, int[]> mStates = new HashMap<>();
    long mNextFrameRenderTime = Long.MIN_VALUE;

    private final Rect mDstRect = new Rect();
    private ColorMatrix mColorFilter = new ColorMatrix();

    /* private ColorStateList mTint;
    private PorterDuffColorFilter mTintFilter; */
    private BlendMode mTintMode;

    private final RenderTask mRenderTask = new RenderTask(this);
    private final Rect mSrcRect;
    private int mScaledWidth;
    private int mScaledHeight;
    private Transform mTransform;

    private Animator mAnimator = new Animator() {
        @Override
        public void start() {
            synchronized (this) {
                if (mIsRunning) {
                    return;
                }
                mIsRunning = true;
            }
            final long lastFrameRemainder = mNativeInfoHandle.restoreRemainder();
            startAnimation(lastFrameRemainder);
        }

        @Override
        public boolean isRunning() {
            return mIsRunning;
        }

        @Override
        public void stop() {
            synchronized (this) {
                if (!mIsRunning) {
                    return;
                }
                mIsRunning = false;
            }

            cancelPendingRenderTask();
            mNativeInfoHandle.saveRemainder();
        }
    };

    /**
     * Creates drawable from resource.
     *
     * @param context context
     * @param res Resources to read from
     * @param id resource id (raw or drawable)
     * @throws IOException when opening failed
     * @throws WrongTypeException WrongTypeException
     * @throws NotExistException NotExistException
     */
    public GifDrawable(Context context, ResourceManager res, int id) throws IOException,
            NotExistException, WrongTypeException {
        this((res.getRawFileEntry(res.getMediaPath(id))).openRawFileDescriptor());
        final float densityScale = GifViewUtils.getDensityScale(context, res, id);
        mScaledHeight = (int) (mNativeInfoHandle.getHeight() * densityScale);
        mScaledWidth = (int) (mNativeInfoHandle.getWidth() * densityScale);
    }

    /**
     * Creates drawable from asset.
     *
     * @param assets AssetManager to read from
     * @param assetName name of the asset
     * @throws IOException when opening failed
     * @throws NullPointerException if assets or assetName is null
     */
    // get the RawFileEntry of the asset name(given its path) and call this constructor
    public GifDrawable(RawFileEntry assets, String assetName) throws IOException {
        this(assets.openRawFileDescriptor());
    }

    /**
     * Constructs drawable from given file path.<br>
     * Only metadata is read, no graphic data is decoded here.
     * In practice can be called from main thread. However it will violate
     * StrictMode policy if disk reads detection is enabled.<br>
     *
     * @param filePath path to the GIF file
     * @throws IOException when opening failed
     * @throws NullPointerException if filePath is null
     */
    public GifDrawable(String filePath) throws IOException {
        this(new GifInfoHandle(filePath), null, null, true);
    }

    /**
     * Equivalent to {@code} GifDrawable(file.getPath())}
     *
     * @param file the GIF file
     * @throws IOException when opening failed
     * @throws NullPointerException if file is null
     */
    public GifDrawable(File file) throws IOException {
        this(file.getPath());
    }

    /**
     * Creates drawable from InputStream.
     * InputStream must support marking, IllegalArgumentException will be thrown otherwise.
     *
     * @param stream stream to read from
     * @throws IOException when opening failed
     * @throws IllegalArgumentException if stream does not support marking
     * @throws NullPointerException if stream is null
     */
    public GifDrawable(InputStream stream) throws IOException {
        this(new GifInfoHandle(stream), null, null, true);
    }

    /**
     * Creates drawable from AssetFileDescriptor.
     * Convenience wrapper for GifDrawable(FileDescriptor)}
     *
     * @param afd source
     * @throws NullPointerException if afd is null
     * @throws IOException IOException
     */
    public GifDrawable(RawFileDescriptor afd) throws IOException {
        this(new GifInfoHandle(afd), null, null, true);
    }

    /**
     * Creates drawable from FileDescriptor
     *
     * @param fd source
     * @throws IOException when opening failed
     * @throws NullPointerException if fd is null
     */
    public GifDrawable(FileDescriptor fd) throws IOException {
        this(new GifInfoHandle(fd), null, null, true);
    }

    /**
     * Creates drawable from byte array.<br>
     * It can be larger than size of the GIF data. Bytes beyond GIF terminator are not accessed.
     *
     * @param bytes raw GIF bytes
     * @throws IOException if bytes does not contain valid GIF data
     * @throws NullPointerException if bytes are null
     */
    public GifDrawable(byte[] bytes) throws IOException {
        this(new GifInfoHandle(bytes), null, null, true);
    }

    /**
     * Creates drawable from {@link ByteBuffer}. Only direct buffers are supported.
     * Buffer can be larger than size of the GIF data. Bytes beyond GIF terminator are not accessed.
     *
     * @param buffer buffer containing GIF data
     * @throws IOException if buffer does not contain valid GIF data or is indirect
     * @throws NullPointerException if buffer is null
     */
    public GifDrawable(ByteBuffer buffer) throws IOException {
        this(new GifInfoHandle(buffer), null, null, true);
    }

    /**
     * Creates drawable uri
     *
     * @param uri GIF Uri, cannot be null.
     * @param resolver resolver used to query {@code uri}, can be null for file:// scheme Uris
     * @throws IOException if resolution fails or destination is not GIF.
     * @throws DataAbilityRemoteException DataAbilityRemoteException
     */
    public GifDrawable(DataAbilityHelper resolver, Uri uri) throws IOException, DataAbilityRemoteException {
        this(GifInfoHandle.openUri(resolver, uri), null, null, true);
    }

    /**
     * Creates drawable from {@link InputSource}.
     *
     * @param inputSource The {@link InputSource} concrete subclass used to construct {@link GifDrawable}.
     * @param oldDrawable The old drawable that will be reused to save the memory. Can be null.
     * @param executor The executor for rendering tasks. Can be null.
     * @param isRenderingTriggeredOnDraw True if rendering of the next frame is scheduled after drawing current one
     * @param options Options controlling various GIF parameters.
     * @throws IOException if input source is invalid.
     * @throws DataAbilityRemoteException DataAbilityRemoteException
     */
    protected GifDrawable(InputSource inputSource,
                          GifDrawable oldDrawable,
                          ScheduledThreadPoolExecutor executor,
                          boolean isRenderingTriggeredOnDraw,
                          GifOptions options) throws IOException, DataAbilityRemoteException {
        this(inputSource.createHandleWith(options), oldDrawable, executor, isRenderingTriggeredOnDraw);
    }

    GifDrawable(GifInfoHandle gifInfoHandle, final GifDrawable oldDrawable, ScheduledThreadPoolExecutor executor,
                boolean isRenderingTriggeredOnDraw) {
        mIsRenderingTriggeredOnDraw = isRenderingTriggeredOnDraw;
        mExecutor = executor != null ? executor : GifRenderingExecutor.getInstance();
        mNativeInfoHandle = gifInfoHandle;
        PixelMap oldPixelMap = null;
        if (oldDrawable != null) {
            synchronized (oldDrawable.mNativeInfoHandle) {
                if (!oldDrawable.mNativeInfoHandle.isRecycled()
                        && oldDrawable.mNativeInfoHandle.getHeight() >= mNativeInfoHandle.getHeight()
                        && oldDrawable.mNativeInfoHandle.getWidth() >= mNativeInfoHandle.getWidth()) {
                    oldDrawable.shutdown();
                    oldPixelMap = oldDrawable.mPixelMap;
                    oldPixelMap.writePixels(0x00000000); // Color.TRANSPARENT
                }
            }
        }
        if (oldPixelMap == null) {
            PixelMap.InitializationOptions initializationOptions = new PixelMap.InitializationOptions();
            initializationOptions.pixelFormat = ohos.media.image.common.PixelFormat.ARGB_8888;
            initializationOptions.size = new Size();
            initializationOptions.size.height = mNativeInfoHandle.getHeight();
            initializationOptions.size.width = mNativeInfoHandle.getWidth();
            mPixelMap = PixelMap.create(initializationOptions);
        } else {
            mPixelMap = oldPixelMap;
        }
        mPixelMap.setAlphaType(gifInfoHandle.isOpaque() ? AlphaType.OPAQUE : AlphaType.PREMUL); // can be ignored
        mSrcRect = new Rect(0, 0, mNativeInfoHandle.getWidth(), mNativeInfoHandle.getHeight());
        mInvalidationHandler = new InvalidationHandler(this);
        mRenderTask.doWork();
        mScaledWidth = mNativeInfoHandle.getWidth();
        mScaledHeight = mNativeInfoHandle.getHeight();
        mPaint.setDither(true);
        mPaint.setFilterBitmap(true);
    }

    /**
     * Frees any memory allocated native way.
     * Operation is irreversible. After this call, nothing will be drawn.
     * This method is idempotent, subsequent calls have no effect.
     * Like recycle() this is an advanced call and
     * is invoked implicitly by finalizer.
     */
    public void recycle() {
        shutdown();
        mPixelMap.release();
    }

    /**
     * Paint used to draw on Canvas
     *
     * @return mPixelMap
     */
    public PixelMap getPixelMap() {
        return mPixelMap;
    }

    private void shutdown() {
        mIsRunning = false;
        mInvalidationHandler.removeEvent(MSG_TYPE_INVALIDATION);
        mNativeInfoHandle.recycle();
    }

    /**
     * isRecycled
     *
     * @return true if drawable is recycled
     */
    public boolean isRecycled() {
        return mNativeInfoHandle.isRecycled();
    }

    /**
     * invalidateSelf
     */
    public void invalidateSelf() {
        /* super.invalidateSelf();*/
        Component callback = getElementCallback();
        if (callback != null) {
            callback.invalidate();
        }
        scheduleNextRender();
    }

    public void setElementCallback(Component callback) {
        mCallback = callback;
    }

    public Component getElementCallback() {
        return mCallback;
    }

    public int getIntrinsicHeight() {
        return mScaledHeight;
    }

    public int getIntrinsicWidth() {
        return mScaledWidth;
    }

    /**
     * setAlpha
     *
     * @param alpha alpha
     */
    public void setAlpha(int alpha) {
        mPaint.setAlpha(alpha);
    }

    /**
     * setColorFilter
     *
     * @param cf colorfilter
     */
    public void setColorFilter(ColorMatrix cf) {
        mPaint.setColorMatrix(cf);
    }

    /**
     * getOpacity
     *
     * @return int
     */
    public int getOpacity() {
        if (!mNativeInfoHandle.isOpaque() || mPaint.getAlpha() < DEF_255) {
            return TRANSPARENT; // PixelFormat.TRANSPARENT is -2
        }
        return OPAQUE; // PixelFormat.OPAQUE is -1
    }

    /**
     * Starts the animation. Does nothing if GIF is not animated.
     * This method is thread-safe.
     */
    /*@Override
    public void start() {
        synchronized (this) {
            if (mIsRunning) {
                return;
            }
            mIsRunning = true;
        }
        final long lastFrameRemainder = mNativeInfoHandle.restoreRemainder();
        startAnimation(lastFrameRemainder);
    }*/

    void startAnimation(long lastFrameRemainder) {
        if (mIsRenderingTriggeredOnDraw) {
            mNextFrameRenderTime = 0;
            mInvalidationHandler.sendEvent(MSG_TYPE_INVALIDATION, 0);
        } else {
            cancelPendingRenderTask();
            mRenderTaskSchedule = mExecutor.schedule(mRenderTask, Math.max(lastFrameRemainder, 0),
                    TimeUnit.MILLISECONDS);
        }
    }

    public Animator getAnimator() {
        return mAnimator;
    }

    /**
     * Causes the animation to start over.
     * If rewinding input source fails then state is not affected.
     * This method is thread-safe.
     */
    public void reset() {
        mExecutor.execute(new SafeRunnable(this) {
            @Override
            public void doWork() {
                if (mNativeInfoHandle.reset()) {
                    mAnimator.start();
                }
            }
        });
    }

    /**
     * Stops the animation. Does nothing if GIF is not animated.
     * This method is thread-safe.
     */
    /*    @Override
    public void stop() {
        synchronized (this) {
            if (!mIsRunning) {
                return;
            }
            mIsRunning = false;
        }

        cancelPendingRenderTask();
        mNativeInfoHandle.saveRemainder();
    } */
    private void cancelPendingRenderTask() {
        if (mRenderTaskSchedule != null) {
            mRenderTaskSchedule.cancel(false);
        }
        mInvalidationHandler.removeEvent(MSG_TYPE_INVALIDATION);
    }

    /*@Override
    public boolean isRunning() {
        return mIsRunning;
    }*/

    /**
     * Returns GIF comment
     *
     * @return comment or null if there is no one defined in file
     */
    public String getComment() {
        return mNativeInfoHandle.getComment();
    }

    /**
     * Returns loop count previously read from GIF's application extension block.
     * Defaults to 1 if there is no such extension.
     *
     * @return loop count, 0 means that animation is infinite
     */
    public int getLoopCount() {
        return mNativeInfoHandle.getLoopCount();
    }

    /**
     * Sets loop count of the animation. Loop count must be in range {@code <0 ,65535>}
     *
     * @param loopCount loop count, 0 means infinity
     */
    public void setLoopCount(final int loopCount) {
        mNativeInfoHandle.setLoopCount(loopCount);
    }

    /**
     * toString
     *
     * @return basic description of the GIF including size and number of frames
     */
    @Override
    public String toString() {
        return String.format(Locale.ENGLISH, "GIF: size: %dx%d, frames: %d, error: %d",
                mNativeInfoHandle.getWidth(), mNativeInfoHandle.getHeight(), mNativeInfoHandle.getNumberOfFrames(),
                mNativeInfoHandle.getNativeErrorCode());
    }

    /**
     * getNumberOfFrames
     *
     * @return number of frames in GIF, at least one
     */
    public int getNumberOfFrames() {
        return mNativeInfoHandle.getNumberOfFrames();
    }

    /**
     * Retrieves last error which is also the indicator of current GIF status.
     *
     * @return current error or {@link GifError#NO_ERROR} if there was no error or drawable is recycled
     */
    public GifError getError() {
        return GifError.fromCode(mNativeInfoHandle.getNativeErrorCode());
    }

    /**
     * createFromResource
     *
     * @param context context
     * @param res res
     * @param resourceId resourceId
     * @return GifDrawable
     */
    public static GifDrawable createFromResource(Context context, ResourceManager res, int resourceId) {
        try {
            return new GifDrawable(context, res, resourceId);
        } catch (IOException | NotExistException | WrongTypeException ignored) {
            return null;
        }
    }

    /**
     * Sets new animation speed factor.<br>
     * Note: If animation is in progress ({@link #draw(Canvas)}) was already called)
     * then effects will be visible starting from the next frame. Duration of the currently rendered
     * frame is not affected.
     *
     * @param factor new speed factor, eg. 0.5f means half speed, 1.0f - normal, 2.0f - double speed
     * @throws IllegalArgumentException if factor&lt;=0
     */
    public void setSpeed(final float factor) {
        mNativeInfoHandle.setSpeedFactor(factor);
    }

    /**
     * Equivalent of {@link #stop()}
     */
    @Override
    public void pause() {
        mAnimator.stop();
    }

    /**
     * Retrieves duration of one loop of the animation.
     * If there is no data (no Graphics Control Extension blocks) 0 is returned.
     * Note that one-frame GIFs can have non-zero duration defined in Graphics Control Extension block,
     * use {@link #getNumberOfFrames()} to determine if there is one or more frames.
     *
     * @return duration of of one loop the animation in milliseconds. Result is always multiple of 10.
     */
    @Override
    public int getDuration() {
        return mNativeInfoHandle.getDuration();
    }

    /**
     * Retrieves elapsed time from the beginning of current loop of animation.
     * If there is only one frame or drawable is recycled zero is returned
     *
     * @return elapsed time from the beginning of loop in ms
     */
    @Override
    public int getCurrentPosition() {
        return mNativeInfoHandle.getCurrentPosition();
    }

    /**
     * Seeks animation to given absolute position (within given loop) and refreshes the canvas.<br>
     * If <code>position</code> is greater than duration of the loop of animation or whole animation if there is no loop
     * then animation will be sought to the end, no exception will be thrown.<br>
     * NOTE: all frames from current (or first one if seeking backward) to desired one must be rendered
     * sequentially to perform seeking.It may take lot of time if number of such frames is large.
     * Method is thread-safe. Decoding is performed in background thread and drawable is invalidated automatically
     * afterwards.
     *
     * @param position position to seek to in milliseconds
     * @throws IllegalArgumentException if <code>position</code>&lt;0
     */
    @Override
    public void seekTo(final int position) {
        if (position < 0) {
            throw new IllegalArgumentException(" Position is not positive");
        }
        mExecutor.execute(new SafeRunnable(this) {
            @Override
            public void doWork() {
                mNativeInfoHandle.seekToTime(position, mPixelMap);
                mGifDrawable.mInvalidationHandler.sendEvent(MSG_TYPE_INVALIDATION, 0);
            }
        });
    }

    /**
     * Like {@link #seekTo(int)} but performs operation synchronously on current thread
     *
     * @param position position to seek to in milliseconds
     * @throws IllegalArgumentException if <code>position</code>&lt;0
     */
    public void seekToBlocking(final int position) {
        if (position < 0) {
            throw new IllegalArgumentException("Position is not positive");
        }

        synchronized (mNativeInfoHandle) {
            mNativeInfoHandle.seekToTime(position, mPixelMap);
        }
        mInvalidationHandler.sendEvent(MSG_TYPE_INVALIDATION, 0);
    }

    /**
     * Like {@link #seekTo(int)} but uses index of the frame instead of time.
     * If <code>frameIndex</code> exceeds number of frames, seek stops at the end, no exception is thrown.
     *
     * @param frameIndex index of the frame to seek to (zero based)
     * @throws IllegalArgumentException if <code>frameIndex</code>&lt;0
     */
    public void seekToFrame(final int frameIndex) {
        if (frameIndex < 0) {
            throw new IndexOutOfBoundsException("Frame index is not positive");
        }
        mExecutor.execute(new SafeRunnable(this) {
            @Override
            public void doWork() {
                mNativeInfoHandle.seekToFrame(frameIndex, mPixelMap);
                mInvalidationHandler.sendEvent(MSG_TYPE_INVALIDATION, 0);
            }
        });
    }

    /**
     * Like {@link #seekToFrame(int)} but performs operation synchronously and returns that frame.
     *
     * @param frameIndex index of the frame to seek to (zero based)
     * @return frame at desired index
     * @throws IndexOutOfBoundsException if frameIndex&lt;0
     */
    public PixelMap seekToFrameAndGet(final int frameIndex) {
        if (frameIndex < 0) {
            throw new IndexOutOfBoundsException("Frame index is not positive");
        }
        final PixelMap pixelMap;
        synchronized (mNativeInfoHandle) {
            mNativeInfoHandle.seekToFrame(frameIndex, mPixelMap);
            pixelMap = getCurrentFrame();
        }
        mInvalidationHandler.sendEvent(MSG_TYPE_INVALIDATION, 0);
        return pixelMap;
    }

    /**
     * Like {@link #seekTo(int)} but performs operation synchronously and returns that frame.
     *
     * @param position position to seek to in milliseconds
     * @return frame at desired position
     * @throws IndexOutOfBoundsException if position&lt;0
     */
    public PixelMap seekToPositionAndGet(final int position) {
        if (position < 0) {
            throw new IllegalArgumentException("Position is not positive");
        }
        final PixelMap pixelMap;
        synchronized (mNativeInfoHandle) {
            mNativeInfoHandle.seekToTime(position, mPixelMap);
            pixelMap = getCurrentFrame();
        }
        mInvalidationHandler.sendEvent(MSG_TYPE_INVALIDATION, 0);
        return pixelMap;
    }

    /**
     * Equivalent of {@link #isRunning()}
     *
     * @return true if animation is running
     */
    @Override
    public boolean isPlaying() {
        return mIsRunning;
    }

    /**
     * Used by MediaPlayer for secondary progress bars.
     * There is no buffer in GifDrawable, so buffer is assumed to be always full.
     *
     * @return always 100
     */
    @Override
    public int getBufferPercentage() {
        return BUFF_PERCENT;
    }

    /**
     * Checks whether pause is supported.
     *
     * @return always true, even if there is only one frame
     */
    @Override
    public boolean canPause() {
        return true;
    }

    /**
     * Checks whether seeking backward can be performed.
     *
     * @return true if GIF has at least 2 frames
     */
    @Override
    public boolean canSeekBackward() {
        return getNumberOfFrames() > 1;
    }

    /**
     * Checks whether seeking forward can be performed.
     *
     * @return true if GIF has at least 2 frames
     */
    @Override
    public boolean canSeekForward() {
        return getNumberOfFrames() > 1;
    }

    /**
     * Used by MediaPlayer.
     * GIFs contain no sound, so 0 is always returned.
     *
     * @return always 0
     */
    @Override
    public int getAudioSessionId() {
        return 0;
    }

    /**
     * Returns the minimum number of bytes that can be used to store pixels of the single frame.
     * Returned value is the same for all the frames since it is based on the size of GIF screen.
     * <p>This method should not be used to calculate the memory usage of the bitmap.
     * Instead see {@link #getAllocationByteCount()}.
     *
     * @return the minimum number of bytes that can be used to store pixels of the single frame
     */
    public int getFrameByteCount() {
        return mPixelMap.getBytesNumberPerRow() * mPixelMap.getImageInfo().size.height;
    }

    /**
     * Returns size of the memory needed to store pixels of this object. It counts possible length of all frame buffers.
     * Returned value may be lower than amount of actually allocated memory if GIF uses dispose to previous method
     * but frame requiring it has never been needed yet. Returned value does not change during runtime.
     *
     * @return possible size of the memory needed to store pixels of this object
     */
    public long getAllocationByteCount() {
        long byteCount = mNativeInfoHandle.getAllocationByteCount();
        byteCount += mPixelMap.getPixelBytesCapacity();

        /* byteCount += getFrameByteCount();*/ // try to use this if the above line is not working
        return byteCount;
    }

    /**
     * Returns the maximum possible size of the allocated memory used to store pixels and metadata of this object.
     * It counts length of all frame buffers. Returned value does not change over time.
     *
     * @return maximum possible size of the allocated memory needed to store metadata of this object
     */
    public long getMetadataAllocationByteCount() {
        return mNativeInfoHandle.getMetadataByteCount();
    }

    /**
     * Returns length of the input source obtained at the opening time or -1 if
     * length cannot be determined. Returned value does not change during runtime.
     * If GifDrawable is constructed from {@link InputStream} -1 is always returned.
     * In case of byte array and {@link ByteBuffer} length is always known.
     * In other cases length -1 can be returned if length cannot be determined.
     *
     * @return number of bytes backed by input source or -1 if it is unknown
     */
    public long getInputSourceByteCount() {
        return mNativeInfoHandle.getSourceLength();
    }

    /**
     * Returns in pixels[] copy of the data in the current frame. Each value is packed int representing  {@link Color}.
     *
     * @param pixels the array to receive the frame's colors
     * @throws ArrayIndexOutOfBoundsException if the pixels array is too small to receive required number of pixels
     */
    public void getPixels(int[] pixels) {
        mPixelMap.readPixels(pixels, 0, 0, new ohos.media.image.common.Rect(0, 0,
            mNativeInfoHandle.getWidth(), mNativeInfoHandle.getHeight()));
    }

    /**
     * Returns the {@link Color} at the specified location. Throws an exception
     * if xory are out of bounds (negative or to the width or height
     * respectively).The returned color is non-premultiplied ARGB value.
     *
     * @param xvalue The x-coordinate (0...width-1) of the pixel to return
     * @param yvalue The y-coordinate (0...height-1) of the pixel to return
     * @return The argb {@link Color} at the specified coordinate
     * @throws IllegalArgumentException if xVal, yVal exceed the drawable's bounds
     * @throws IllegalStateException if drawable is recycled
     */
    public int getPixel(int xvalue, int yvalue) {
        if (xvalue >= mNativeInfoHandle.getWidth()) { // need to check explicitly because reused bitmap may be larger
            throw new IllegalArgumentException("x must be < width");
        }
        if (yvalue >= mNativeInfoHandle.getHeight()) {
            throw new IllegalArgumentException("y must be < height");
        }
        return mPixelMap.readPixel(new ohos.media.image.common.Position(xvalue, yvalue));
    }

    /**
     * onBoundsChange
     *
     * @param bounds bounds
     */
    protected void onBoundsChange(Rect bounds) {
        mDstRect.set(bounds.left, bounds.top, bounds.right, bounds.bottom);
        if (mTransform != null) {
            mTransform.onBoundsChange(bounds);
        }
    }

    @Override
    public void setBounds(Rect bounds) {
        super.setBounds(bounds);
        onBoundsChange(bounds);
    }

    /**
     * Reads and renders new frame if needed then draws last rendered frame.
     *
     * @param canvas canvas to draw into
     */
    @Override
    public void drawToCanvas(Canvas canvas) {
        final boolean clearColorFilter;

        // todo - mTintFilter
        if (mPaint.getColorMatrix() == null) {
            /* mPaint.setColorMatrix(mTintFilter);*/
            clearColorFilter = true;
        } else {
            clearColorFilter = false;
        }
        if (mTransform == null) {
            RectFloat src = new RectFloat(mSrcRect.left, mSrcRect.top, mSrcRect.right, mSrcRect.bottom);
            RectFloat dst = new RectFloat(mDstRect.left, mDstRect.top, mPixelMap.getImageInfo().size.width,
                    mPixelMap.getImageInfo().size.height); // no callback found in hmos for onboundschange
            canvas.drawPixelMapHolderRect(new PixelMapHolder(mPixelMap), src, dst, mPaint);
        } else {
            mTransform.onDraw(canvas, mPaint, mPixelMap);
        }
        if (clearColorFilter) {
            mPaint.setColorFilter(null);
        }
    }

    private void scheduleNextRender() {
        if (mIsRenderingTriggeredOnDraw && mIsRunning && mNextFrameRenderTime != Long.MIN_VALUE) {
            /* final long renderDelay = Math.max(240, mNextFrameRenderTime - Time.getRealActiveTime());*/
            mNextFrameRenderTime = Long.MIN_VALUE;
            mExecutor.remove(mRenderTask);
            mRenderTaskSchedule = mExecutor.schedule(mRenderTask, RENDER_DELAY, TimeUnit.MILLISECONDS);
        }
    }

    /**
     * getPaint
     *
     * @return the paint used to render this drawable
     */
    public final Paint getPaint() {
        return mPaint;
    }

    @Override
    public int getAlpha() {
        return (int)mPaint.getAlpha();
    }

    /**
     * setFilterBitmap
     *
     * @param filter filter
     */
    public void setFilterBitmap(boolean filter) {
        mPaint.setFilterBitmap(filter);
        invalidateSelf();
    }

    /**
     * setDither
     *
     * @param dither dither
     */
    public void setDither(boolean dither) {
        mPaint.setDither(dither);
        invalidateSelf();
    }

    /**
     * Adds new animation listener
     *
     * @param listener animation listener to be added, not null
     * @throws NullPointerException if listener is null
     */
    public void addAnimationListener(AnimationListener listener) {
        mListeners.add(listener);
    }

    /**
     * Removes an animation listener
     *
     * @param listener animation listener to be removed
     * @return true if listener collection has been modified
     */
    public boolean removeAnimationListener(AnimationListener listener) {
        return mListeners.remove(listener);
    }

    public ColorMatrix getColorMatrix() {
        return mPaint.getColorMatrix();
    }

    /**
     * Retrieves copy of currently buffered frame.
     *
     * @return current frame
     */
    public PixelMap getCurrentFrame() {
        PixelMap.InitializationOptions options = new PixelMap.InitializationOptions();
        options.pixelFormat = mPixelMap.getImageInfo().pixelFormat;
        options.editable = mPixelMap.isEditable();
        options.alphaType = mPixelMap.getImageInfo().alphaType;
        final PixelMap copy = PixelMap.create(mPixelMap, options);
        return copy;
    }

    /* private PorterDuffColorFilter updateTintFilter(ColorStateList tint, BlendMode tintMode) {
        if (tint == null || tintMode == null) {
            return null;
        }
        final int color = tint.getColorForState(getState(this), Color.TRANSPARENT.getValue());
        return new PorterDuffColorFilter(color, tintMode);
       } */

    @Override
    public void addState(int[] stateSet, Element element) {
        super.addState(stateSet, element);
        mStates.put(element, stateSet);
        onStateChange(stateSet);
    }

    /**
     * getState
     *
     * @param element element
     * @return int array
     */
    public int[] getState(Element element) {
        return mStates.get(element);
    }

    /*@Override
    public void setTintList(ColorStateList tint) {
        mTint = tint;
        mTintFilter = updateTintFilter(tint, mTintMode);
        invalidateSelf();
    }*/

    /***
     * setTintMode
     *
     * @param tintMode tintMode
     */
    public void setTintMode(BlendMode tintMode) {
        mTintMode = tintMode;
        /* mTintFilter = updateTintFilter(mTint, tintMode);*/
        invalidateSelf();
    }

    /**
     * onStateChange
     *
     * @param stateSet stateSet
     * @return boolean
     */
    protected boolean onStateChange(int[] stateSet) {
        // todo
        if (mTintMode != null) {
            /* mTintFilter = updateTintFilter(mTint, mTintMode);*/
            return true;
        }
        return false;
    }

    @Override
    public boolean isStateful() {
        // todo: mTint.isStateful()
        return super.isStateful() /*|| (mTint != null && mTint.isStateful())*/;
    }

    /**
     * Sets whether this drawable is visible. If rendering of next frame is scheduled on draw current one (the default)
     * then this method only calls through to the super class's implementation.<br>
     * Otherwise (if {@link GifDrawableBuilder#setRenderingTriggeredOnDraw(boolean)} was used with <code>true</code>)
     * when the drawable becomes invisible, it will pause its animation. A
     * subsequent change to visible with <code>restart</code> set to true will
     * restart the animation from the first frame. If <code>restart</code> is
     * false, the animation will resume from the most recent frame.
     *
     * @param visible true if visible, false otherwise
     * @param restart when visible and rendering is triggered on draw, true to force the animation to restart
     * from the first frame
     *
     * @return true if the new visibility is different than its previous state
     */
    @Override
    public boolean setVisible(boolean visible, boolean restart) {
        final boolean changed = super.setVisible(visible, restart);
        if (!mIsRenderingTriggeredOnDraw) {
            if (visible) {
                if (restart) {
                    reset();
                }
                if (changed) {
                    mAnimator.start();
                }
            } else if (changed) {
                mAnimator.stop();
            }
        }
        return changed;
    }

    /**
     * Returns zero-based index of recently rendered frame in given loop or -1 when drawable is recycled.
     *
     * @return index of recently rendered frame or -1 when drawable is recycled
     */
    public int getCurrentFrameIndex() {
        return mNativeInfoHandle.getCurrentFrameIndex();
    }

    /**
     * Returns zero-based index of currently played animation loop. If animation is infinite or
     * drawable is recycled 0 is returned.
     *
     * @return index of currently played animation loop
     */
    public int getCurrentLoop() {
        final int currentLoop = mNativeInfoHandle.getCurrentLoop();
        if (currentLoop == 0 || currentLoop < mNativeInfoHandle.getLoopCount()) {
            return currentLoop;
        } else {
            return currentLoop - 1;
        }
    }

    /**
     * Returns whether all animation loops has ended. If drawable is recycled false is returned.
     *
     * @return true if all animation loops has ended
     */
    public boolean isAnimationCompleted() {
        return mNativeInfoHandle.isAnimationCompleted();
    }

    /**
     * Returns duration of the given frame (in milliseconds). If there is no data (no Graphics
     * Control Extension blocks or drawable is recycled) 0 is returned.
     *
     * @param index index of the frame
     * @return duration of the given frame in milliseconds
     * @throws IndexOutOfBoundsException if index &lt; 0 or index &gt;= number of frames
     */
    public int getFrameDuration(final int index) {
        return mNativeInfoHandle.getFrameDuration(index);
    }

    /**
     * Sets the corner radius to be applied when drawing the bitmap.
     * Note that changing corner radius will cause replacing current {@link Paint} shader by BitmapShader.
     * Transform set by setTransform(Transform)} will also be replaced
     *
     * @param cornerRadius corner radius or 0 to remove rounding
     */
    public void setCornerRadius(final float cornerRadius) {
        mTransform = new CornerRadiusTransform(cornerRadius);
        mTransform.onBoundsChange(mDstRect);
    }

    /**
     * getCornerRadius
     *
     * @return The corner radius applied when drawing this drawable. 0 when drawable is not rounded.
     */
    public float getCornerRadius() {
        if (mTransform instanceof CornerRadiusTransform) {
            return ((CornerRadiusTransform) mTransform).getCornerRadius();
        }
        return 0;
    }

    /**
     * Specify {@link Transform} implementation to customize how the GIF's current Bitmap is drawn.
     *
     * @param transform new {@link Transform} or null to remove current one
     */
    public void setTransform(Transform transform) {
        mTransform = transform;
        if (mTransform != null) {
            mTransform.onBoundsChange(mDstRect);
        }
    }

    /**
     * getTransform
     *
     * @return The current {@link Transform} implementation that customizes
     * how the GIF's current Bitmap is drawn or null if nothing has been set.
     */
    public Transform getTransform() {
        return mTransform;
    }
}
